/*
 * Copyright 2012 Alain Pannetrat (L1L1@gmx.com)
 * 
 * This file is part of cookie-miner.
 * 
 * cookie-miner is free software: you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * cookie-miner is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License 
 * along with cookie-miner. If not, see http://www.gnu.org/licenses/.
 *
 * @version: $Id$
 *
 */
#include <pthread.h>
#include "vectors.h"
#include <unistd.h>
#include <queue>
#include <sys/time.h>
#include "graphs.h"
#include "tcp.h"
#include "bufio.h"
#include "json.h"
#include <locale.h>
#include <string.h>
#include "log_facility.h"
#include "common.h"
#include "gfx_engine.h"
#include "quadtree.h"

static Universe UNIVERSE(0,0,100,100);
#define BORDER 5
bool cookies_only = false;
bool display_data = false;

/****************************************************************
 *
 *
 *  Universe container
 *
 */
 

namespace gfx {
	class UniverseEngine : public Engine {
		public:
			UniverseEngine() : Engine() {}
			bool idle();

		private:
			DISALLOW_COPY_AND_ASSIGN(UniverseEngine);
	};

	class UniverseContainer: public Container {
		public:
			UniverseContainer() : 
				Container(0,0), hover_node(NULL), ref_time(0) {}

			virtual bool resize();
			virtual bool on_click(const Event& ev);
			virtual bool on_release(const Event& ev);
			virtual bool on_double_click(const Event& ev);
			virtual bool on_mouse_move(const Event& ev);
			virtual bool on_key_press(const Event& ev);

			virtual bool draw();
		private:
			Vertex* hover_node;
			unsigned ref_time;

			DISALLOW_COPY_AND_ASSIGN(UniverseContainer);
	};
}

/****************************************************************
 *
 *
 *  GTK UI STUFF STARTS HERE
 *
 */

//
// Borrowed this below from http://slabode.exofire.net/circle_draw.shtml
//

void CreateCircle(GLfloat *retval, double cx, double cy, double r, int num_segments) 
{ 
	double theta = 2 * M_PI / double(num_segments); 
	double tangetial_factor = tanf(theta); //calculate the tangential factor 

	double radial_factor = cosf(theta); //calculate the radial factor 
	
	double x = r; //we start at angle = 0 

	double y = 0; 
    
	for(int ii = 0; ii < num_segments; ii++) 
	{ 
		retval[3*ii+0]= x + cx;
	        retval[3*ii+1]= y + cy; 
	        retval[3*ii+2]= 0; 		 
        
		//calculate the tangential vector 
		//remember, the radial vector is (x, y) 
		//to get the tangential vector we flip those coordinates and negate one of them 

		double tx = -y; 
		double ty = x; 
        
		//add the tangential vector 

		x += tx * tangetial_factor; 
		y += ty * tangetial_factor; 
        
		//correct using the radial factor 

		x *= radial_factor; 
		y *= radial_factor; 
	}
}

void DrawCircle(double cx, double cy, double r, int num_segments) 
{ 
	GLfloat coords[900];

	if (num_segments>300) num_segments = 300;
	
	CreateCircle(coords,cx,cy,r,num_segments);
	glVertexPointer(3, GL_FLOAT, 0, coords);
	glEnableClientState(GL_VERTEX_ARRAY);
	glDrawArrays(GL_LINE_LOOP, 0, num_segments);
	glDisableClientState(GL_VERTEX_ARRAY);
}

void DrawPoint(double cx, double cy, double r, int num_segments) 
{ 
	GLfloat coords[900];

	if (num_segments>300) num_segments = 300;
	
	CreateCircle(coords,cx,cy,r,num_segments);
	
	glEnableClientState(GL_VERTEX_ARRAY);
	glVertexPointer(3, GL_FLOAT, 0, coords);
	glDrawArrays(GL_POLYGON, 0, num_segments);
	glVertexPointer(3, GL_FLOAT, 0, coords);
	glDrawArrays(GL_LINE_LOOP, 0, num_segments);
	glDisableClientState(GL_VERTEX_ARRAY);
}


#define RADIUS_F 1.25f
void screen_draw_point(Vertex *v) 
{
	const Vector& color = v->color();		
	const Vector& pos(v->position());
	int vradius = v->radius();

	glLineWidth(1.0);    

	if (v->selected())
	{
		glColor3f(0,1,0);
		DrawCircle(/*BORDER+*/pos[0], /*BORDER+*/pos[1],vradius*RADIUS_F+3,(int)(vradius+3)*4);
	}

	glColor3f(color[0],color[1],color[2]);
	//DrawPoint(BORDER+pos[0], BORDER+pos[1],vradius,(int)vradius*2);

	gfx::Engine::RoundPoint.blit(/*BORDER+*/pos[0]-vradius*RADIUS_F,
				     /*BORDER+*/pos[1]-vradius*RADIUS_F,
			             /*BORDER+*/pos[0]+vradius*RADIUS_F,
				     /*BORDER+*/pos[1]+vradius*RADIUS_F);
}

void screen_draw_edge(Vertex* src, Edge *e, double dim)
{
    const Vector& src_pos(src->position());
    int src_radius = src->radius();
    const Vector& dst_pos(e->destination()->position());
    int dst_radius = e->destination()->radius();
    Vector color(dim*e->color());
    double dx = (dst_pos[0]-src_pos[0]);
    double dy = (dst_pos[1]-src_pos[1]);
    double norm = sqrt(dx*dx+dy*dy);

    glPushMatrix();

    glColor3f(color[0],color[1],color[2]);

    glLineWidth(1.5);    

    glTranslatef(/*BORDER+*/src_pos[0],/*BORDER+*/src_pos[1],0);

    if (dx<0)
	glRotatef((atan(dy/dx)*180/M_PI)+180,0,0,1);
    else if (dx>0)
	glRotatef((atan(dy/dx)*180/M_PI),0,0,1);

    glBegin(GL_LINES);
    	glVertex2f(src_radius+3,0);
    	glVertex2f(norm-(dst_radius+3),0);

	glVertex2f(norm-(dst_radius+7),-3);
	glVertex2f(norm-(dst_radius+3),0);

	glVertex2f(norm-(dst_radius+7),3);
	glVertex2f(norm-(dst_radius+3),0);
    glEnd();

    glPopMatrix();
}

void screen_draw_dir(Vertex* src, Vertex *dst)
{
    const Vector& pos(src->position());
    double w,h;

    if (src->name().size()==0 || !gfx::Engine::SansSerifFont.loaded()) return;

    gfx::Engine::SansSerifFont.extent(w,h,src->name());

    glPushMatrix();

    glColor3f(0,1,0);
    
    glTranslatef(/*BORDER+*/pos[0],/*BORDER+*/pos[1],0);

    if (dst)
    {
    	    Vector dir = src->position() - dst->position();

	    if (dir[0]>0)
	    { 
		    glRotatef(atan(dir[1]/dir[0])*180.0/M_PI,0,0,1);
		    gfx::Engine::SansSerifFont.render_text(2*src->radius(),h/2,src->name());
	    }
	    else if (dir[0]<0)
	    {	
		    glRotatef(atan(dir[1]/dir[0])*180.0/M_PI,0,0,1);
		    gfx::Engine::SansSerifFont.render_text(-w-2*src->radius(),h/2,src->name());
	    }
	    else
	    {
		    gfx::Engine::SansSerifFont.render_text(2*src->radius(),h/2,src->name());
	    }
    }
    else
	    gfx::Engine::SansSerifFont.render_text(2*src->radius(),h/2,src->name());

    glPopMatrix();
}

void screen_draw_rect(float left, float top, float right, float bottom)
{
    glColor3f(0.25,0.25,0.25);
    glLineWidth(0.5);    
    glBegin(GL_LINE_LOOP);
    	glVertex2f(left,top);
    	glVertex2f(right,top);
	glVertex2f(right,bottom);
	glVertex2f(left,bottom);
    glEnd();
}


/*********************************************************************
 * 
 *  Data processing thread.
 *
 */

pthread_mutex_t item_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
std::queue<Json::Value*> item_queue;

void *process_input_thread(void *ctx_ptr)
{
	int socket;
	BufferInOutSocket *buf_socket;
	char server[128];

	logger.new_thread();

	if (strchr((const char *)ctx_ptr,':')==NULL)
		snprintf(server,80,"%s:9998",(const char *)ctx_ptr);
	else
		snprintf(server,80,"%s",(const char *)ctx_ptr);

	logger.message(logger.LOG_DEBUG,"Started client-server input processing thread");

	socket = tcp_connect(server);

	if (socket<0) 
	{
		logger.message(logger.LOG_ERROR,"Failed to connect to %s, aborting...",server);
		exit(2);
	}

	logger.message(logger.LOG_DEBUG,"Created socket %i to %s",socket,server);	

	buf_socket = BufferInOutSocket::create(socket);

	while (!buf_socket->read_end())
	{
		Json::Value* json;
		
		buf_socket->write_line("more\r\n");
		buf_socket->write_flush();

		json=Json::unserialize(buf_socket);

		if (json->get("message")->equals("graph"))
		{

			if (json->get("items")->size()==0)
			{
				sleep(1);
			}
			else
			{
				for (unsigned i=0;i<json->get("items")->size();i++)
				{
					Json::Value *ref = json->get("items")->get(i);
					if (ref->is_defined())
					{
						pthread_mutex_lock(&item_queue_mutex);
 						item_queue.push(ref->reference());
						pthread_mutex_unlock(&item_queue_mutex);
					}
				}
			}	
		}
		else if (json->get("message")->equals("error"))
		{
			logger.message(logger.LOG_ERROR,"Server error: %s",json->get("details")->as_cstring());
			sleep(5);
		}
		else
		{
			logger.message(logger.LOG_ERROR,"Unrecognized message from server, pausing 5 seconds...");
			sleep(5);
		}

		// FIXME
		Json::unreference(json);
	}

	delete buf_socket;
	tcp_close(socket);
	return NULL;
}

void process_input(Json::Value* jobj)
{
	Vertex *src;
	Vertex *dst;
	Vector color;
	std::string source;
	std::string destination;
	float x1,y1,x2,y2;

	UNIVERSE.get_limits(x1,y1,x2,y2);

	if (jobj->get("type")->equals("vertex") && !cookies_only)
	{
		source = jobj->get("source")->as_string();

		src = UNIVERSE.get_vertex_by_name(source);
		if (!src) 
			src = Vertex::create(UNIVERSE,Vector((x2+x1)/2+rand()%32,(y2+y1)/2+rand()%32,0),source);

		if (jobj->get("color")->is_defined())
		{
			color = Vector( jobj->get("color")->get(0)->as_double(),
					jobj->get("color")->get(1)->as_double(),
					jobj->get("color")->get(2)->as_double());
			src->color(color);
		}
	}
	else if (jobj->get("type")->equals("edge"))
	{

		source      = jobj->get("source")->as_string();
		destination = jobj->get("destination")->as_string();
		color = Vector( jobj->get("color")->get(0)->as_double(),
				jobj->get("color")->get(1)->as_double(),
				jobj->get("color")->get(2)->as_double());
	

		if (!cookies_only || (color[2]>0.1 && cookies_only)) 
		{

			src = UNIVERSE.get_vertex_by_name(source);
			if (!src) 
				src = Vertex::create(UNIVERSE,Vector(rand()%32,rand()%32,0),source);

			dst = UNIVERSE.get_vertex_by_name(destination);
			if (!dst) 
				dst = Vertex::create(UNIVERSE,Vector(src->position()[0]+BORDER,src->position()[1]+BORDER,0),destination);
			if (!src->has_edge_to(dst))
			    Edge::create(src,dst,color);
		}
	}
}

bool gfx::UniverseEngine::idle()
{
	int count=10;
	int sz;
	Json::Value *val;

	while (count--)
	{
		val = NULL;
		pthread_mutex_lock(&item_queue_mutex);
		if (item_queue.size()>0)
		{
			sz = item_queue.size();
			val = item_queue.front();
			item_queue.pop();
		}
		pthread_mutex_unlock(&item_queue_mutex);

		if (val)
		{
			process_input(val); 
			Json::unreference(val);
		}
		else
			return true;
	}

	return true;
}


/*********************************************************************
 * 
 *  Universe Containers Implementation
 *
 */

bool draw_area(QNode *nd,void *dummy)
{
	float x1,y1,x2,y2;
	nd->area(x1,y1,x2,y2);
	screen_draw_rect(BORDER+x1,BORDER+y1,BORDER+x2,BORDER+y2);
	DrawCircle(BORDER+nd->position()[0],BORDER+nd->position()[1],5,20);
	return true;
}

bool gfx::UniverseContainer::resize()
{
	UNIVERSE.set_limits(BORDER,
			    BORDER,
			    position().width()-BORDER,
			    position().height()-BORDER);

	return true;
}

static int count_skip = 0;
static float average_draw_time = 0;
static float average_comp_time = 0;

bool gfx::UniverseContainer::draw()
{
	Vertex *v;
	char buf[100];
	unsigned time_beg;
	unsigned time_med;
	unsigned time_end;
	double dim;

	// 
	// Reduce framerate when energy is low
	//
	//if (UNIVERSE.energy()<0.01 && count_skip++<5) 
	//	return false;
	count_skip = 0;

	// 
	// Compute universe graph
	//

	time_beg = gfx::Engine::time();

	UNIVERSE.animate(1.0/100);

	//
	// Draw universe
	//

	time_med = gfx::Engine::time();
	
	glClear(GL_COLOR_BUFFER_BIT);
	
	glLoadIdentity();

	for (GraphSet::iterator git=UNIVERSE.g_begin(); git!=UNIVERSE.g_end(); ++git)
	{
	    for (VertexList::iterator vit=(*git)->v_begin(); vit!=(*git)->v_end(); ++vit)
	    {
		v = *vit;

		for (EdgeList::iterator eit=v->e_begin();eit!=v->e_end();++eit)
		{
		    Edge *e = *eit;
		    Vertex *n = e->destination();

		    
		    if ((hover_node && n!=hover_node && v!=hover_node)
			|| (UNIVERSE.vertex_selected_count() && (!n->selected() || !v->selected())))
			dim = 0.5;
		    else
			dim = 1.0;

		    screen_draw_edge(v,e,dim);
		}
	
		screen_draw_point(v);
		if (v->tagged()) screen_draw_dir(v,NULL);
	    }
	}

	if (hover_node)
	{
	    for (EdgeList::iterator eit=hover_node->e_begin();eit!=hover_node->e_end();++eit)
	    {
		Edge *e = *eit;
		Vertex *n = e->destination();

		if (!n->tagged()) screen_draw_dir(n,hover_node);
	    }

	    if (!hover_node->tagged()) screen_draw_dir(hover_node,NULL);
	}

	time_end = gfx::Engine::time();


	if (display_data)
	{
		glColor4f(0,0.5,0,0.8);	

		glBegin(GL_POLYGON);
		glVertex2f(0,				position().height()-20);
		glVertex2f(position().width()-1,	position().height()-20);
		glVertex2f(position().width()-1,	position().height()-1);
		glVertex2f(0,				position().height()-1);
		glEnd();

		glColor4f(0.8,0.8,0.8,1.0);	

		average_comp_time = ((49*average_comp_time)+1.0*(time_med-time_beg))/50.0;
		average_draw_time = ((49*average_draw_time)+1.0*(time_end-time_med))/50.0;

		sprintf(buf,"e=%4.2g p=%u s=%u fps=%u dur=(%2.2f+%2.2f)",
				log10(UNIVERSE.energy()),
				UNIVERSE.size(),
				UNIVERSE.vertex_selected_count(),
				1000/(time_beg-ref_time+1),
				average_comp_time,
				average_draw_time);

		gfx::Engine::SansSerifFont.render_text(0,position().height()-5,buf);
	}

	ref_time = time_beg;

	return gfx::Container::draw();
}


bool gfx::UniverseContainer::on_click(const Event& ev)
{
	Vertex *v;

	UNIVERSE.suspended_vertex(NULL);
	
	if (Container::on_click(ev)) return true;	

	if (ev.button.button==SDL_BUTTON_LEFT)
	{
		if ((v=UNIVERSE.vertex_locate(Vector(ev.button.x,ev.button.y,0),3))!=NULL)
		{
			UNIVERSE.suspended_vertex(v);
			//UNIVERSE.vertex_selected(v,!UNIVERSE.vertex_selected(v));
			return true;
		}
	}
	return false;
}

bool gfx::UniverseContainer::on_release(const Event& ev)
{
	if (Container::on_release(ev)) return true;	
	UNIVERSE.suspended_vertex(NULL);
	return true;
}

bool gfx::UniverseContainer::on_double_click(const Event& ev)
{
	Vertex *v;

	if (Container::on_double_click(ev)) return true;	

	if (ev.button.button==SDL_BUTTON_LEFT)
	{
		if ((v=UNIVERSE.vertex_locate(Vector(ev.button.x,ev.button.y,0),3))!=NULL)
		{
			v->selected(!v->selected());
			return true;
		}
	}
	return false;
}

bool gfx::UniverseContainer::on_mouse_move(const Event& ev)
{
	if (Container::on_mouse_move(ev)) return true;	

	if (ev.motion.state&SDL_BUTTON(1))
	{
		if (UNIVERSE.suspended_vertex()) 
			UNIVERSE.suspended_vertex()->position(Vector(ev.motion.x,ev.motion.y,0));
		return true;
	}
	
	hover_node = UNIVERSE.vertex_locate(Vector(ev.motion.x,ev.motion.y,0),3);
	return true;
}

bool gfx::UniverseContainer::on_key_press(const Event& ev)
{
    switch (ev.key.keysym.unicode) {
	case 'c':
	    for (GraphSet::iterator git=UNIVERSE.g_begin();git!=UNIVERSE.g_end();++git)
		for (VertexList::iterator vit=(*git)->v_begin();vit!=(*git)->v_end();++vit)
		{
		    Vertex *v = *vit;

		    if (v->selected())
		    {
			Vector acolor = v->color();
			if (acolor == Vector(1,1,1)) acolor = Vector(1,0,0);
			else if (acolor == Vector(1,0,0)) acolor = Vector(0,1,0);
			else if (acolor == Vector(0,1,0)) acolor = Vector(0,0,1);
			else if (acolor == Vector(0,0,1)) acolor = Vector(1,1,0);
			else if (acolor == Vector(1,1,0)) acolor = Vector(0,1,1);
			else if (acolor == Vector(0,1,1)) acolor = Vector(1,0,1);
			else acolor = Vector(1,1,1);
			v->color(acolor);
		    }
		}
	    return true;
	case 's':
	    for (GraphSet::iterator git=UNIVERSE.g_begin();git!=UNIVERSE.g_end();++git)
		for (VertexList::iterator vit=(*git)->v_begin();vit!=(*git)->v_end();++vit)
		{
		    Vertex *v = *vit;

		    if (v->selected())
		    {
			double aradius = v->radius()+2;
			if (aradius>10) aradius=5;
			v->radius(aradius);		
		    }
		}
	    return true;
	case 't':
	    for (GraphSet::iterator git=UNIVERSE.g_begin();git!=UNIVERSE.g_end();++git)
		for (VertexList::iterator vit=(*git)->v_begin();vit!=(*git)->v_end();++vit)
		{
		    Vertex *v = *vit;

		    if (v->selected())
			v->tagged(!v->tagged());
		}
	    return true;
	    //case 'g':
	    //	UNIVERSE.gravity(!UNIVERSE.gravity());
	    //	return true;
	case 'd':
	    display_data = !display_data;
	    return true;
	case SDLK_ESCAPE:
	    for (GraphSet::iterator git=UNIVERSE.g_begin();git!=UNIVERSE.g_end();++git)
		for (VertexList::iterator vit=(*git)->v_begin();vit!=(*git)->v_end();++vit)
		{
		    Vertex *v = *vit;

		    v->selected(false);
		}
	    return true;
	default:
	    /* nothing */
	    break;
    }
    return false;
}

/********************************************************************
 */


int main( int   argc, 
	  char *argv[] )
{
	gfx::UniverseEngine engine;
	gfx::UniverseContainer *ucontainer = new gfx::UniverseContainer;
	char* server;
	pthread_t thread_desc;

	setlocale(LC_ALL,"C");

	engine.init(ucontainer);

	engine.screen_setup(false,800,600);

	if (argc>1)
		server = argv[1];
	else
		server = "localhost:9998";

	pthread_create(&thread_desc,NULL,&process_input_thread,server);
	pthread_detach(thread_desc);

	engine.run();

	logger.message(logger.LOG_DEBUG,"Exiting program normally");

	return 0;
}

